/**
 * Copyright (C) 2017 - 2020 https://github.com/joewee
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.bifrost.game.service.impl;

import com.bifrost.game.entity.LotteryAward;
import com.bifrost.game.entity.LotteryGame;
import com.bifrost.game.entity.LotteryGameAward;
import com.bifrost.game.entity.LotteryGameWinner;
import com.bifrost.game.event.LotteryGameWinEvent;
import com.bifrost.game.exception.LotteryUnavailableException;
import com.bifrost.game.repository.LotteryGameAwardRepository;
import com.bifrost.game.repository.LotteryGameRepository;
import com.bifrost.game.repository.LotteryGameWinnerRepository;
import com.bifrost.game.service.LotteryGameService;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.jpa.criteria.expression.function.AggregationFunction;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 抽奖游戏相关服务类
 *
 * @author joewee
 * @version 1.0.0
 * @Date 2018/5/9 10:38
 */
@Service
public class LotteryGameServiceImpl implements LotteryGameService {
    @Autowired
    LotteryGameRepository gameRepository;
    @Autowired
    LotteryGameWinnerRepository gameWinnerRepository;
    @Autowired
    LotteryGameAwardRepository gameAwardRepository;
    @Autowired
    ApplicationContext applicationContext;

    @Autowired
    RedissonClient redisson;
    private static final Logger LOGGER = LoggerFactory.getLogger(LotteryGameService.class);


    /**
     * 查询对应门店的所有有效活动
     *
     * @param merchanId
     * @return
     */
    @Override
    public List<LotteryGame> findGamesByMerchant(Long merchanId) {
        return null;
    }

    /**
     * 根据uid查询抽奖游戏
     *
     * @param uid
     * @return
     */
    @Override
    public LotteryGame findByUid(String uid) {
        Assert.hasText(uid, "uid不能为空");
        return gameRepository.findFirstByUid(uid);

    }

    @Override
    public LotteryGame findId(Long uid) {
        return gameRepository.findOne(uid);
    }

    /**
     * 根据id查询抽奖游戏
     *
     * @param id
     * @return
     */
    @Override
    public LotteryGame findById(Long id) {
        Assert.notNull(id, "id不能为空");
        return gameRepository.findOne(id);
    }

    /**
     * 参与抽奖
     * @param game
     * @return
     */
    @Override
    @Transactional(rollbackFor = {Exception.class})
    public int attendLotteryGame(LotteryGame game){
        return  gameRepository.incrementAttendTimes(game.getId(),1);
    }
    /**
     * 抽奖
     * <p>若中奖则返回LotteryGameWinner抽奖结果，未中奖则为null</p>
     *
     * @param customerId 用户标识
     * @param uid        抽奖游戏uid
     * @return
     */
    @Override
    @Transactional(rollbackFor = {Exception.class})
    public LotteryGameWinner draw(String customerId, Long uid) throws LotteryUnavailableException {
        Assert.notNull(uid, "uid不能为空");
        Assert.hasText(customerId, "用户标识不能为空");
        RLock lock = redisson.getLock(String.format("%s.%s", LotteryGame.LOCK_KEY_PERFIX, uid));
        try {
            boolean locked = lock.tryLock(10, 5, TimeUnit.SECONDS);
            if (locked) {
                LotteryGame game = findById(uid);
                if (game == null) {
                    throw new LotteryUnavailableException("抽奖活动不存在");
                } else if (!Boolean.TRUE.equals(game.isEnabled())) {
                    throw new LotteryUnavailableException("抽奖活动已经结束");
                } else if (!game.isActive()) {
                    throw new LotteryUnavailableException("抽奖活动未开始或已失效");
                } else {
                    //TODO 验证用户是否达到抽奖条件
                    /**
                     * 1、获取游戏剩余最大抽奖次数=奖品上设置的最大次数之和-游戏已抽次数
                     * 2、计算每个等级概率=该等级奖品剩余/剩余最大抽奖次数
                     * 3、抽奖
                     * */
                    //已参与次数
                    Integer attendCount = game.getAttendCount();
                    List<LotteryGameAward> awards = game.getLotteryGameAwards();

                    List<Double> prob = new ArrayList<Double>();
                    if (!CollectionUtils.isEmpty(awards)) {
                        final List<Integer> quantityList = new ArrayList(awards.size());
                        //final int[] quantity = {0};
                        final int[] sumTimes = {0};
                        int remainTimes = 0;
                        awards.stream().forEach(lotteryGameAward -> {
                            quantityList.add(lotteryGameAward.getQuantityAvailable());
                            sumTimes[0] += lotteryGameAward.getMaxTimes();
                        });
                        remainTimes = sumTimes[0] - attendCount;
                        if (remainTimes <= 0) {
                            throw new LotteryUnavailableException("抽奖活动已经结束");
                        }
                        BigDecimal sumProbability = BigDecimal.ZERO;
                        for (Integer quantity : quantityList) {
                            //计算中奖概率
                            BigDecimal probability = BigDecimal.valueOf(quantity).divide(BigDecimal.valueOf(remainTimes), 4, BigDecimal.ROUND_HALF_UP);
                            prob.add(probability.doubleValue());
                            //总中奖概率
                            sumProbability=sumProbability.add(probability);
                        }
                        if(sumProbability.compareTo(BigDecimal.ONE)>0){
                          throw new RuntimeException("抽奖游戏奖项配置错误:中奖概率大于1");
                        }else if(sumProbability.compareTo(BigDecimal.ONE)<0){
                            awards.add(null);
                            prob.add(BigDecimal.ONE.subtract(sumProbability).doubleValue());
                        }
                        //奖项必须与概率项数一致
                        Assert.isTrue(prob.size()==awards.size(),"抽奖游戏异常");
                        AliasMethod aliasMethod = new AliasMethod(prob);
                        int gameAwardIndex = aliasMethod.next();
                        LotteryGameAward lotteryGameAward = awards.get(gameAwardIndex);
                        if(lotteryGameAward==null){
                            attendLotteryGame(game);
                            return null;
                        }else{
                            //TODO
                            int updated = attendLotteryGame(game);
                            if(updated==1){
                                //记录中奖信息并扣减奖品数量
                               LotteryGameWinner winner = writeWinnerLog(game,customerId,lotteryGameAward);
                                return  winner;
                            }else{
                                return null;
                            }
                        }
                    }
                }
            }
            return null;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (lock.isLocked()) {
                lock.unlock();
            }
        }
    }
    @Override
    @Transactional(rollbackFor = {Exception.class})
    public LotteryGameWinner writeWinnerLog(LotteryGame game, String customerId, LotteryGameAward lotteryGameAward) {
        LotteryAward award = lotteryGameAward.getLotteryAward();
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("抽奖游戏{}{}获得{}奖品{}",game.getName(),customerId,lotteryGameAward.getGrade(),award.getName());
        }
        LotteryGameWinner winner =new LotteryGameWinner();
        winner.setWinnerId(customerId);
        winner.setQuantity(1);
        winner.setLotteryGame(game);
        winner.setLotteryAward(lotteryGameAward.getLotteryAward());
        winner = gameWinnerRepository.save(winner);
        String uid = UUID.randomUUID().toString();
        //随机生成兑换码：id+4位随机数字
        winner.setExchangeCode(String.format ("%d%s",winner.getId(),StringUtils.left(uid,4)));
        gameWinnerRepository.save(winner);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("开始扣减奖品数量");
        }
        int done = gameAwardRepository.decrementQuantity(lotteryGameAward.getId(),1);
        if(done!=1){
            throw new RuntimeException("更新奖品数量出错");
        }else{
            LotteryGameWinEvent event =new LotteryGameWinEvent(winner);
            applicationContext.publishEvent(event);
            return  winner;
        }
    }
    @Override
    public LotteryGameAward findLotteryGameAward(LotteryGame game, LotteryAward award){
        Assert.notNull(game,"游戏不能为空");
        Assert.notNull(award,"奖品不能为空");
        return gameAwardRepository.findFirstByLotteryGameAndLotteryAward(game,award);
}
    /**
     * Alias算法实现抽奖
     */
    private class AliasMethod {
        private final double[] probability;
        private final int[] alias;
        private final int length;
        private final Random rand;

        public AliasMethod(List<Double> prob) {
            this(prob, new Random());
        }

        public AliasMethod(List<Double> prob, Random rand) {
            /* Begin by doing basic structural checks on the inputs. */
            if (prob == null || rand == null) {
                throw new NullPointerException();
            }
            if (prob.size() == 0) {
                throw new IllegalArgumentException("Probability vector must be nonempty.");
            }
            this.rand = rand;
            this.length = prob.size();
            this.probability = new double[length];
            this.alias = new int[length];
            double[] probtemp = new double[length];
            Deque<Integer> small = new ArrayDeque<Integer>();
            Deque<Integer> large = new ArrayDeque<Integer>();
            /* divide elements into 2 groups by probability */
            for (int i = 0; i < length; i++) {
                 /* initial probtemp */
                probtemp[i] = prob.get(i) * length;
                if (probtemp[i] < 1.0) {
                    small.add(i);
                } else {
                    large.add(i);
                }
            }


            while (!small.isEmpty() && !large.isEmpty()) {
                int less = small.pop();
                int more = large.pop();
                probability[less] = probtemp[less];
                alias[less] = more;
                probtemp[more] = probtemp[more] - (1.0 - probability[less]);
                if (probtemp[more] < 1.0) {
                    small.add(more);
                } else {
                    large.add(more);
                }
            }
        /*
        * At this point, everything is in one list, which means that the
        * remaining probabilities should all be 1/n. Based on this, set them
        * appropriately.
        */
            while (!small.isEmpty()) {
                probability[small.pop()] = 1.0;
            }
            while (!large.isEmpty()) {
                probability[large.pop()] = 1.0;
            }
        }

        /**
         * Samples a value from the underlying distribution.
         */
        public int next() {
        /* Generate a fair die roll to determine which column to inspect. */
            int column = rand.nextInt(length);
        /* Generate a biased coin toss to determine which option to pick. */
            boolean coinToss = rand.nextDouble() < probability[column];
        /* Based on the outcome, return either the column or its alias. */
            return coinToss ? column : alias[column];
        }
    }
}
